/*
 * Copyright 2007 National Bioinformatics Network
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package dgbgui.selection;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Observable;
import java.util.Set;

import dgbgui.modeldocument.Query;

public class SelectionManager extends Observable  {
	
	public static enum ObservedEventType {
		ADD,
		REMOVE
	}
	
	public class ObservableData {
		
		public ObservableData(ObservedEventType type) {
			m_type = type;
			m_items = new LinkedHashSet<Object>();
		}
		
		public ObservableData(ObservedEventType type, Set<Object> nodes) {
			m_type = type;
			m_items = new LinkedHashSet<Object>();
			m_items.addAll(nodes);
		}
		
		public ObservedEventType getType() {
			return m_type;
		}
		
		public void addSet(Set<Object> items) {
			for ( Object item : items ) {
				assert(!m_items.contains(item));
			}
			m_items.addAll(items);
		}
		
		public Set<Object> getItems() {
			return new HashSet<Object>(m_items);
		}
		
		public int size() {
			return m_items.size();
		}
		
		ObservedEventType m_type;
		Set<Object> m_items;
	}
	
	public static SelectionManager getInstance() {
		if ( m_instance == null ) {
			m_instance = new SelectionManager();
		}
		return m_instance;
	}
	
    // Private for singleton
	private SelectionManager() {
	}
	
	
	/**
	 * Get the selection filtered to a certain type
	 * @param classType The class type to filter with
	 * @return The filtered selection
	 */
	public Set<Query> getDefinitionFilteredSelection(Class classType) {
		Set<Query> filteredSelection = new LinkedHashSet<Query>();
		for ( Object obj : getSelection(Query.class) ) {
            Query node = (Query)obj;
			if ( classType.isInstance(node.getDefinition()) ) {
				filteredSelection.add(node);
			}
		}
		return filteredSelection;
	}
	

	private void addSet(Class type, Set<Object> selection ) {
        assert (type.isInstance(selection.iterator().next()));
        for ( Object obj : selection ) {
            assert (!contains(type,obj));
        }
        
        if ( canSelect(type,selection) ) {
            // If no items are currently selected, or if there is a selection
            // and the type is the same type of the new item, add to the
            // selection
            if ( ( size() == 0 ) ||
                 ( ( size() > 0 ) && typeAlreadySelected(type) ) ) {
                if ( !m_selection.containsKey(type) ) {
                    m_selection.put(type, new LinkedHashSet<Object>());
                }
                // Add the item of not already in the list
                m_selection.get(type).addAll(selection);
                ObservableData addedItems = new ObservableData(ObservedEventType.ADD,selection);
                setChanged();
                notifyObservers(addedItems);
            }
        }
	}
    
    private void removeSet(Class type, Set<Object> selection ) {
        assert (type.isInstance(selection.iterator().next()));
        for ( Object obj : selection ) {
            assert (contains(type,obj));
        }
        
        m_selection.get(type).removeAll(selection);
        ObservableData removedItems = new ObservableData(ObservedEventType.REMOVE,selection);
        setChanged();
        notifyObservers(removedItems);
    }
    
    /**
     * Add a set of item to the selection
     * @param type the class type of the items
     * @param selection the set of items
     */
    public void xorSet(Class type, Set<Object> selection ) {
        Set<Object> toAdd = new LinkedHashSet<Object>();
        Set<Object> toRemove = new LinkedHashSet<Object>();
        for ( Object obj : selection) {
            // Add new items, remove already contained items
            if ( contains(type,obj) ) {
                toRemove.add(obj);
            } else  {
                toAdd.add(obj);
            }
        }
        
        if ( toRemove.size() > 0 ) {
            removeSet(type,toRemove);
        }
        if ( toAdd.size() > 0 ) {
            addSet(type,toAdd);
        }
    }
	
	
	/**
     * Set the selection the the given set of objects
	 * @param type
	 * @param newSelection
	 */
    public void setSet(Class type, Set<Object> newSelection) {
        if ( canSelect(type,newSelection)) {;
            clear();
            addSet(type,newSelection);
        }
    }
	

//	public void remove(Class type, Object item) {
//		assert (contains(type,item));
//		
//        m_selection.get(type).remove(item);
//        
//		ObservableData removedItems = new ObservableData(ObservedEventType.REMOVE,item);
//		setChanged();
//		notifyObservers(removedItems);
//	}
	
	public boolean contains(Class type, Object item) {
        if ( m_selection.containsKey(type) ) {
            return m_selection.get(type).contains(item);
        }
        return false;
	}
	
    public LinkedHashSet<Object> getSelection(Class type) {
        if ( m_selection.containsKey(type) ) {
            return m_selection.get(type);
        }
        return new LinkedHashSet<Object>();
    }
    
    /**
     * @return the currently selected type, null if no selection
     */
    public Class selectedType() {
        for ( Class selectedType : m_selection.keySet() ) {
            if ( m_selection.get(selectedType).size() > 0 ) {
                return selectedType;
            }
        }
        return null;
    }
    
	/**
	 * @return the number of selected objects
	 */
	public int size(Class type) {
        if ( m_selection.containsKey(type)) {
            return m_selection.get(type).size();
        }
		return 0;
	}
    
    /**
     * @return the number of selected objects regardless of type
     */
    public int size() {
        int size = 0;
        for ( Class selectedType : m_selection.keySet() ) {
            size += m_selection.get(selectedType).size(); 
        }
        return size;
    }
	
	public void clear() {
        ObservableData observableItems = new ObservableData(ObservedEventType.REMOVE);   
        
        for ( Class selectedType : m_selection.keySet() ) {
            if ( m_selection.get(selectedType).size() > 0 ) {
                observableItems.addSet(m_selection.get(selectedType));
            }
        }
        
        m_selection = new HashMap<Class,LinkedHashSet<Object>>();
	
		// Notify if there were nodes added
        setChanged();	
        notifyObservers(observableItems);
    }
	
    
    /**
     * Determines if the given class type is already selected
     * @param type
     * @return true if the type is already selected
     */
    private boolean typeAlreadySelected(Class type) {
        // An item of a certain class is selectable if there is no selection,
        // or if items of that class are already selected
        for ( Class selectedType : m_selection.keySet() ) {
            if ( ( type != selectedType ) &&
                 ( m_selection.get(selectedType).size() != 0 ) ) {
                return false;
            }
        }
        return true;
    }
    
    /**
     * Determine if the type & instance can be selected
     */
    private boolean canSelect(Class toSelectType, Set<Object> newSelection) {
        // If not of the selectable class, can't select
        if ( ( m_selectableClasses.size() > 0 ) && 
             ( !m_selectableClasses.contains(toSelectType) ) ) {
            return false;
        }
        // If not of a selectable instance, can't select
        if (m_selectableInstances.size() > 0 ) {
            for ( Object obj : newSelection ) {
                if ( !m_selectableInstances.contains(obj)) {
                    return false;
                }
            }
        }
        return true;
    }

    public void setSelectableInstances(Set<Object> selectables) {
        m_selectableInstances = selectables;
    }
    
    /**
     * Set the list of classes that can be selected
     * @param selectables the classes that can be selected
     */
    public void setSelectableTypes(Set<Class> selectables) {
        m_selectableClasses = selectables;
    }
    
    /**
     * Restore the selection manager so that all classes are selectable,
     * but still contrained to the same type of class once a single
     * type has been selected
     */
    public void restoreSelectable() {
        m_selectableClasses = new HashSet<Class>();
        m_selectableInstances = new HashSet<Object>();
    }
    
    // The map of classes to selected objects
    HashMap<Class,LinkedHashSet<Object>> m_selection = new HashMap<Class,LinkedHashSet<Object>>();
	
	private static SelectionManager m_instance;
    
    // The list of classes that can be selected
    Set<Class> m_selectableClasses = new HashSet<Class>();
    // The instances that may be selected 
    Set<Object> m_selectableInstances = new HashSet<Object>();

	
}
